我們在昨天有留下一個問題,假設今天變成是 AVG的外接口想要去轉成 HDMI,那是不是就要再多一個 AvgToHdmiAdapter 的類別出來並且一樣去繼承 HDMI呢?今天就要利用『Strategy』模式來解決!
定義一系列的演算法,把它們一個個封裝起來,並且使它們可相互替換。Strategy 模式使演算法可獨立於使用它的客戶而變化。

(圖片來源:https://www.dofactory.com/img/diagrams/net/strategy.png)
我們先回顧一下昨天利用 Output 類別整合所有接口的 UML

這邊我想要做以下幾點的改變
ScreenService 的服務,其中會去判斷如果螢幕的接口規格是電腦沒有的,那就呼叫 Adapter 進行轉換。Adapter 獨立出來並定義為介面,所有轉接類別繼承自 Adapter。這裡會將 Adapter 定為介面的原因主要是我希望轉接頭類別是能夠去自己改寫 Switch() 這個行為,而不是都繼承自一個抽象類別,假設今天有個新的轉接方式可能是透過藍芽或其他非接口對接的方式,這樣在起初的定義上就不會過於侷限在一定要是接口,而是在「轉換」這個行為上。ScreenSwitchDic 去存取當外部規格對應到電腦的接口規格時,是使用哪個轉接頭做轉接。新的UML圖

using System;
using System.Collections.Generic;
namespace DAY09_Strategy
{
    class Program
    {
        static void Main(string[] args)
        {
            // 給定螢幕的接口規格
            string screenSpec = "AVG";
            ScreenService screenService = new ScreenService();
            // 判斷是否需要做轉換
            if (screenService.CanConnect(screenSpec))
            {
                Console.WriteLine("不用轉換,直接連線!");
            }
            else
            {
                // 取得對應的電腦接口規格
                var name = ScreenSwitchDic.GetSpec(screenSpec);
                // 使用反射,將對應到的規格字串轉成實體類別
                Type type = Type.GetType($"DAY09_Strategy.{name}");
                var adapter = Activator.CreateInstance(type);
                //使用轉接器,並將所需的轉接類別帶入
                screenService.UseAdapter(screenSpec, (Adapter)adapter);
            }
        }
    }
    public static class ScreenSwitchDic
    {
        // 先定義外部規格對應到電腦的接口規格時,是使用哪個轉接頭做轉接
        public static Dictionary<string, string> switchDic = new Dictionary<string, string>()
        {
            { "AVG", "HDMIAdapter"},
            { "VGA", "USBAdapter"},
            { "DVI", "DisplayPortAdapter"},
        };
        public static string GetSpec(string screenSpec)
        {
            if (switchDic.ContainsKey(screenSpec))
            {
                return switchDic[screenSpec];
            }
            else
            {
                return "找不到對應的規格!";
            }
        }
    }
    public class ScreenService
    {
        // 電腦有的外接規格
        private readonly List<string> computerSpec = new List<string>() { "DisplayPort", "USB", "HDMI" };
        public bool CanConnect(string screenSpec)
        {
            return computerSpec.Exists(x => x == screenSpec);
        }
        // 透過聚合取代繼承的方式,將所需要的轉接規格類別當作參數帶入
        public void UseAdapter(string screenSpec, Adapter adapter)
        {
            adapter.Switch(screenSpec);
        }
    }
    public interface Adapter
    {
        public void Switch(string screenSpec);
    }
    public class DisplayPortAdapter : Adapter
    {
        public void Switch(string screenSpec)
        {
            Console.WriteLine($"已將{screenSpec}轉換成DisplayPort!");
        }
    }
    public class USBAdapter : Adapter
    {
        public void Switch(string screenSpec)
        {
            Console.WriteLine($"已將{screenSpec}轉換成USB!");
        }
    }
    public class HDMIAdapter : Adapter
    {
        public void Switch(string screenSpec)
        {
            Console.WriteLine($"已將{screenSpec}轉換成HDMI!");
        }
    }
}
-結果

透過 Strategy 模式,我們成功解決了昨天提到的問題,現在不用再為每一種不同的轉換規格做特殊化的 Adapter,只需要將我所擁有的接口規格繼承自抽象類別,接著在我們要做轉換時,將需要的接口規格類別帶入,就可以很簡單的進行抽換,並且能夠減少過度繼承的事情發生。
在上述提到的模式三大要點中,最讓我一開始一頭霧水是第二點。不對吧!物件導向中引以為傲的一點就是「繼承」阿,怎麼現在叫我少點使用。但看完 Strategy 模式後,發現原來不是說不要使用繼承,而是要考量到什麼才是我們真正需要關注的。